Repository navigation
feat(transcript): browse and search without disrupting playback - #72
Merged
Merged
Conversation
Adds explicit search-result activation ahead of the reading-mode work
(docs/specs/transcript-reading-mode.md, step 1 of 3):
- Search gains a synchronous onqueryinput(value) callback fired from the
raw input event, and onmatchactivate(annotation, index) fired from
Enter or a "Go to match" button (shown only when showActivation is
true). Search stays playback-agnostic — it only renders the control
and emits callbacks.
- TranscriptSearch owns playback wiring: its default onmatchactivate
seeks once through a new handleMatchActivate transcript-context
action; its default onqueryinput forwards to an optional
handleQueryInput context action, the seam a later step fills in to
enter Browsing. Either default is overridden by a supplied prop, same
as onmatchchange.
- Transcript gains searchSeekBehavior ("change" | "activate", default
"change"). In "change" mode, handleMatchChange keeps seeking on every
match selection (legacy behavior, unchanged). In "activate" mode it
only updates highlight/selection state; handleMatchActivate is the
only seek path.
- Enter handles IME composition (compositionstart/end plus
event.isComposing) and a debounce race: it commits the live input
value before activating, so it never activates a stale match:
selection resets to index 0 only when the committed query actually
changes, preserving a browsed-to index when it hasn't.
Reading-mode controls, the sync concurrency fix, and custom-element
configuration are deferred to steps 2 and 3.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KfYGrLi7iJoevyVQsn5ur
…oll-to-seek concurrency fix Implements step 2 of 3 of the transcript-reading-mode spec (docs/specs/ transcript-reading-mode.md): synchronization policy plus the reading and return controls, building on step 1's search-activation split. - Transcript gains a bindable readingMode (default false) and scrollToSeek (default true). readingMode doubles as the Following/Browsing state itself; the transcript context exposes it plus enterBrowsing/ returnToFollowing actions for custom controls, panel-local by construction (each Transcript instance owns its own state). - The former auto-scroll pause button is replaced by a single "Follow along" switch (role="switch", aria-checked, data-following) — turning it off enters Browsing, which stops both media->scroll and scroll->seek via two independent SyncController gates. A "Jump to current" button appears only while Browsing; returning (via the switch, the button, the context action, or a host property write) scrolls the passage at the media's CURRENT time into view — active cue, next cue when between cues, first/ last at the boundaries, nothing with no annotations — without seeking. Focus moves from Jump to current to the switch only when the Jump button currently owns it; every other path preserves existing focus. The autoscrollPause/autoscrollResume i18n strings are superseded by followAlong/jumpToCurrent. - Concurrency fix: videoController now takes a queuedEpoch captured at invoke time and a getCurrentEpoch() closure reading the live machine snapshot, checked immediately before seekTo() — the transition guard alone was too late, since the actor calls seekTo() itself before its promise resolves. syncMachine's scrollToSeekEnabled gate also blocks entering (and re-entering) scrollDriven outright while disabled, and disabling never replays once re-enabled since the dropped actor has already completed. - Deliberate user scrolling enters Browsing when scrollToSeek is configured off (SyncController reports it via a new onUserScroll hook, after the same auto-scroll-echo suppression and throttle that already gate TRANSCRIPT_SCROLL); programmatic movement — returning, and any scrollToAnnotation call, including TranscriptSegments' own, now delegated through the transcript context when available — marks the scroll programmatic first so it can never be mistaken for that gesture. - handleMatchChange now also checks !readingMode, so Browsing takes precedence over legacy "change"-mode seek-on-select; handleQueryInput (the seam step 1 left optional) now enters Browsing on a nonempty query in "activate" mode. Covers the acceptance-gate rows assigned to this step: scroll-with-seek- off, enable-reading-mode-while-seek-on, return inside-cue, return between-cue, policy-off-mid-seek (+ no replay), two-panel independence, return focus ownership vs host property write, and single-folded-control, plus machine/actor-level coverage of the epoch check and a regression test proving typing in "activate" mode now enters Browsing end-to-end. Deferred to a later step (not required by this step's acceptance rows): clearing the query and selection on canvas switch in "activate"/reading- mode-active (needs a bindable query or context reset signal on Search, same seam step 1 flagged); suppressing segment activation on a text- selection pointer release. The custom element and docs are step 3. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015KfYGrLi7iJoevyVQsn5ur
Step 3 of the transcript-reading-mode spec (docs/specs/transcript-reading-mode.md):
adds search-seek-behavior, scroll-to-seek, and reading-mode attributes/
properties to <iiif-transcript-player>, with the same defaults and
semantics as the Svelte Transcript component.
The element owns value-based parsing and reflection for scroll-to-seek
and reading-mode itself, through canonicalizeBooleanAttr plus a pair of
accessor overrides in extend() — not Svelte's presence-based
type:"Boolean" (which can't tell reading-mode="false" from "true") and
not Svelte-generated reflect:true (which would fight a manual write).
A user-driven readingMode change reflects back onto reading-mode through
a guarded, deferred write that compares against the live attribute and
skips when unchanged, so attributeChangedCallback cannot re-enter and
loop; reflection never touches playback.
The element exposes :state(browsing) (alongside the existing
:state(playing)/:state(loading)/:state(error)) and renders a styled
"Follow along" switch and "Jump to current" button in its shadow DOM —
Transcript.svelte already renders both bare; this is the element's own
skin, consistent with its other control-bar hooks.
Covers the "Custom element configured through markup and properties" and
the element half of "Single folded control across surfaces" acceptance-
gate rows: markup attributes, property writes, properties set before
upgrade, attribute removal, an invalid value's one-time host error,
scroll-to-seek="false" parsing to false, and browsing emitting no seeked
event. A final test walks the recommended configuration
(searchSeekBehavior="activate", scrollToSeek={false}) through the spec's
closing journey: listen, search, browse, activate, browse again, return.
Updates custom-elements.json and public-types.d.ts for the three new
attributes/properties and the browsing custom state, documents the
Transcript/TranscriptSearch additions and the recommended configuration
in the README, and updates the four docs-site demos to use
searchSeekBehavior="activate"/scrollToSeek={false} with a small styled
skin for the new Follow along switch and Jump to current button.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KfYGrLi7iJoevyVQsn5ur
Fixes five gaps found in adversarial review of the transcript reading-mode feature (docs/specs/transcript-reading-mode.md): 1. TranscriptSearch's showActivation now also renders while Browsing (readingMode=true) even in legacy searchSeekBehavior="change", so there's still an explicit way to jump to a match once Browsing suppresses seek-on-select. (Already staged; this adds test coverage.) 2. Canvas-switch query clearing: Transcript bumps a new queryResetSignal on annotation replacement in "activate" mode or while reading mode is active; Search honors a resetSignal prop by clearing its query/selection (and the visible input) without seeking. Legacy "change" mode with reading mode off is unaffected. 3. Segment no longer activates (seeks) on a click that ends a drag-to-select gesture — a non-collapsed text selection at click time suppresses onclick. Keyboard activation (Enter/Space) is unaffected. 4. Search gains onmatchnavigate, fired only from explicit prev/next navigation (never typing or activation); TranscriptSearch wires its default to scrollToAnnotation so browsing search results scrolls them into view. 5. Transcript announces Following/Browsing mode changes through the existing polite live region, using new transcript.followingAnnouncement / transcript.browsingAnnouncement i18n strings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015KfYGrLi7iJoevyVQsn5ur
Add the onmatchnavigate callback row to the Search/TranscriptSearch API table and note that the activation control also appears while Browsing, matching the shipped behavior. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015KfYGrLi7iJoevyVQsn5ur
… and legacy nav - SyncController: replace the fixed 600ms programmatic-scroll suppression window with an explicit scroll transaction closed by `scrollend` (plus a 1s fallback timer), so a smooth scroll that outlasts the old timeout no longer misclassifies its own echo as user input. - TranscriptSegments: guard the custom `segment` snippet's `onclick` with the same text-selection check Segment.svelte already applies, via a new shared `isTextSelectionActive()` helper in transcript/utils.ts, so a drag-to-select release no longer activates a custom-snippet segment. - TranscriptSearch: scope the default `onmatchnavigate` to scroll only in `activate` mode or while Browsing, restoring the legacy `change`-mode behavior (match change + seek via `onmatchchange`, no explicit programmatic scroll) for prev/next navigation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015KfYGrLi7iJoevyVQsn5ur
…xed window The 1s fallback in beginProgrammaticScroll fired even mid-animation, so a smooth scroll longer than 1s released suppression and its later echo events were misread as user scrolls (seek / enter Browsing). Re-arm the fallback on every echo scroll so it is a quiet-period settle detector: it can only elapse once scrolling stops, never during an in-flight scroll, whatever its duration. scrollend remains the primary close; the timer covers no-scrollend engines and the target-already-in-view (no scroll) case. Also confirms overlapping transactions collapse into one without a premature close. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015KfYGrLi7iJoevyVQsn5ur
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Browse and search without disrupting playback
Adds an opt-in reading mode so a listener can search, browse, or read another passage without accidentally moving playback. Additive and backward-compatible: with no new props,
Transcriptand<iiif-transcript-player>keep today's exact behavior (search-driven and scroll-driven seeking).Public surface
TranscriptsearchSeekBehavior: "change" | "activate""change"(legacy)TranscriptscrollToSeek: booleantrueTranscriptreadingMode: booleanfalseSearch/TranscriptSearchonmatchactivate,onqueryinput,onmatchnavigate<iiif-transcript-player>search-seek-behavior,scroll-to-seek,reading-modeattrs + properties,:state(browsing)readingMode. Following: playback may scroll the panel (and seek ifscrollToSeek). Browsing: detached — playback still updates the active-passage highlight but never moves the panel, and scrolling never seeks.role="switch",aria-checked,data-following); the custom element ships a styled shadow pair and reflectsreading-mode.searchSeekBehavior="activate",scrollToSeek={false}.Concurrency
The scroll-driven seek actor (
videoController) callsseekToinside the actor afterwaitForReadyresolves — before its promise resolves — so guarding the consuming transition is too late. The fix captures a policy epoch when the seek is queued and compares it immediately beforeseekTo; disabling scroll-to-seek or enabling reading mode mid-seek drops the pending seek, and re-enabling never replays it (e6fe65b).How it was built
Three reviewable TDD commits, then a parallel adversarial review over the whole diff:
29882c7— separate search match updates from activatione6fe65b— reading mode, the Follow along control, scroll-to-seek concurrency fix27dd4b8— expose reading-mode configuration on the custom element2c8011a— address 5 review findings (activation control visibility while Browsing; canvas-switch query clearing; text-selection must not seek; prev/next scrolls match into view; polite mode-change announcement)27118e3— README: documentonmatchnavigateand control visibility823tests, typecheck, and lint pass. Behavioral cases from the spec's acceptance table are automated at the search, transcript/sync, and custom-element boundaries. Manual verification still recommended for keyboard-only use, screen-reader announcements/focus, text selection, touch scrolling, and reduced motion.🤖 Generated with Claude Code
https://claude.ai/code/session_015KfYGrLi7iJoevyVQsn5ur